Вы работаете в стартапе, который продаёт продукты питания. Нужно разобраться, как ведут себя пользователи вашего мобильного приложения.
Изучите воронку продаж. Узнайте, как пользователи доходят до покупки. Сколько пользователей доходит до покупки, а сколько — «застревает» на предыдущих шагах? На каких именно?
После этого исследуйте результаты A/A/B-эксперимента. Дизайнеры захотели поменять шрифты во всём приложении, а менеджеры испугались, что пользователям будет непривычно. Договорились принять решение по результатам A/A/B-теста. Пользователей разбили на 3 группы: 2 контрольные со старыми шрифтами и одну экспериментальную — с новыми. Выясните, какой шрифт лучше.
Создание двух групп A вместо одной имеет определённые преимущества. Если две контрольные группы окажутся равны, вы можете быть уверены в точности проведенного тестирования. Если же между значениями A и A будут существенные различия, это поможет обнаружить факторы, которые привели к искажению результатов. Сравнение контрольных групп также помогает понять, сколько времени и данных потребуется для дальнейших тестов.
В случае общей аналитики и A/A/B-эксперимента работайте с одними и теми же данными. В реальных проектах всегда идут эксперименты. Аналитики исследуют качество работы приложения по общим данным, не учитывая принадлежность пользователей к экспериментам.
В стартапе, который продаёт продукты питания нужно разобраться, как ведут себя пользователи мобильного приложения. Также необходимо помочь дизайнерам проверитьвлияние изменённых шрифтов на поведение пользователей.
Необходимо произвести анализ воронки продаж с целью проверки её эффективности в "доведении" пользователя до покупкм.
После необходимо проанализировать А/А/В тест для выяснения влияния изменения шрифта сайта на поведение пользователей.
Результаты изложить в общих выводах и рекомендациях для отдела маркетинга.
В исходных предоставленных данных каждая запись в логе — это действие пользователя, или событие.
Изучите результаты эксперимента Статистический анализ А/А теста. Статистический анализ А/В теста. Сравнение всех данных по группам и событиям. Проверка гипотез.
# импортируем необходимые для работы библиотеки
import pandas as pd
import matplotlib.pyplot as plt
%matplotlib inline
%config InlineBackend.figure_format='retina'
import seaborn as sns
import numpy as np
from scipy import stats as st
from datetime import datetime, timedelta
import math as mth
from plotly import graph_objects as go
# откроем файл с данными
logs_exp = pd.read_csv('/datasets/logs_exp.csv', sep='\t')
# переименуем столбцы
logs_exp.rename(
columns = {
'EventName':'event_name',
'DeviceIDHash':'user_id',
'EventTimestamp':'event_time',
'ExpId':'group'}, inplace = True
)
logs_exp.columns = logs_exp.columns.str.lower()
# поменяем формат ячейки
logs_exp['event_time'] = pd.to_datetime(logs_exp['event_time'], unit='s')
# создадим столбец с датой
logs_exp['event_date'] = logs_exp.event_time.dt.date
print(f'количество дубликатов:{logs_exp.duplicated().sum()}')
print(logs_exp.head())
количество дубликатов:413
event_name user_id event_time group \
0 MainScreenAppear 4575588528974610257 2019-07-25 04:43:36 246
1 MainScreenAppear 7416695313311560658 2019-07-25 11:11:42 246
2 PaymentScreenSuccessful 3518123091307005509 2019-07-25 11:28:47 248
3 CartScreenAppear 3518123091307005509 2019-07-25 11:28:47 248
4 PaymentScreenSuccessful 6217807653094995999 2019-07-25 11:48:42 248
event_date
0 2019-07-25
1 2019-07-25
2 2019-07-25
3 2019-07-25
4 2019-07-25
# удалим дубликаты
logs_exp = logs_exp.drop_duplicates().reset_index(drop=True)
# выясним,есть ли пользователи, попавшие в обе группы сразу
len(logs_exp.groupby('user_id')['group'].nunique().reset_index().query('group > 1'))
0
# Посмотрим, что у нас осталось
temp = logs_exp.copy()
list_c = ['event_name', 'user_id', 'event_time', 'group', 'event_date']
print(temp.info())
for col_l in list_c:
print('-'* 25)
print(col_l, temp[col_l].sort_values().unique())
print(col_l,': кол-во NaN',temp[col_l].isna().sum(),
', процент NaN', round(temp[col_l].isna().sum()/len(temp)*100, 2),'%')
<class 'pandas.core.frame.DataFrame'> RangeIndex: 243713 entries, 0 to 243712 Data columns (total 5 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 event_name 243713 non-null object 1 user_id 243713 non-null int64 2 event_time 243713 non-null datetime64[ns] 3 group 243713 non-null int64 4 event_date 243713 non-null object dtypes: datetime64[ns](1), int64(2), object(2) memory usage: 9.3+ MB None ------------------------- event_name ['CartScreenAppear' 'MainScreenAppear' 'OffersScreenAppear' 'PaymentScreenSuccessful' 'Tutorial'] event_name : кол-во NaN 0 , процент NaN 0.0 % ------------------------- user_id [ 6888746892508752 6909561520679493 6922444491712477 ... 9220879493065341500 9221926045299980007 9222603179720523844] user_id : кол-во NaN 0 , процент NaN 0.0 % ------------------------- event_time ['2019-07-25T04:43:36.000000000' '2019-07-25T11:11:42.000000000' '2019-07-25T11:28:47.000000000' ... '2019-08-07T21:14:43.000000000' '2019-08-07T21:14:58.000000000' '2019-08-07T21:15:17.000000000'] event_time : кол-во NaN 0 , процент NaN 0.0 % ------------------------- group [246 247 248] group : кол-во NaN 0 , процент NaN 0.0 % ------------------------- event_date [datetime.date(2019, 7, 25) datetime.date(2019, 7, 26) datetime.date(2019, 7, 27) datetime.date(2019, 7, 28) datetime.date(2019, 7, 29) datetime.date(2019, 7, 30) datetime.date(2019, 7, 31) datetime.date(2019, 8, 1) datetime.date(2019, 8, 2) datetime.date(2019, 8, 3) datetime.date(2019, 8, 4) datetime.date(2019, 8, 5) datetime.date(2019, 8, 6) datetime.date(2019, 8, 7)] event_date : кол-во NaN 0 , процент NaN 0.0 %
Данные подготовлены к последующей обработке:мы заменили названия столбцов на более удобные, проверили пропуски и типы данных, поменяли тип данных в столбце даты и времени, добавили отдельный столбец дат, удалили дубликаты, проверили корректность данных на предмет наличия пользователей, попавших в две группы сразу.
# выясним,сколько событий в логе, сколько пользователей в логе исколько событий приходится в среднем на пользователя
print(f'Всего в логе осталось {len(logs_exp)} событий.')
print(f'Всего пользователей в логе {len(logs_exp.user_id.unique())}.')
print(f'В среднем на пользователя приходится {int(len(logs_exp) / len(logs_exp.user_id.unique()))} события.')
Всего в логе осталось 243713 событий. Всего пользователей в логе 7551. В среднем на пользователя приходится 32 события.
# проверим, данными за какой период мы располагаем, найдём максимальную и минимальную дату.
print(logs_exp.event_time.min())
print(logs_exp.event_time.max())
2019-07-25 04:43:36 2019-08-07 21:15:17
# Построим гистограмму по времени события
logs_exp['event_time'].hist(bins=14*24, figsize=(14, 5))
plt.title("Распределение событий по временным проимежуткам")
plt.xlabel("Время события")
plt.ylabel("Суммарное количество событий")
plt.xticks(rotation = 60)
plt.show()
# очистим данные от устаревших
logs_exp_new = logs_exp.query('event_date >= datetime(2019, 8, 1).date()')
print(logs_exp_new.info())
<class 'pandas.core.frame.DataFrame'> Int64Index: 240887 entries, 2826 to 243712 Data columns (total 5 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 event_name 240887 non-null object 1 user_id 240887 non-null int64 2 event_time 240887 non-null datetime64[ns] 3 group 240887 non-null int64 4 event_date 240887 non-null object dtypes: datetime64[ns](1), int64(2), object(2) memory usage: 11.0+ MB None
print(f'количество событий первой недели {(len(logs_exp) - len(logs_exp_new))}')
print(f'Доля событий первой недели {1 -len(logs_exp_new)/len(logs_exp)}')
print()
print(f'Всего пользователей первой недели {len(logs_exp.user_id.unique())-len(logs_exp_new.user_id.unique())}.')
print(f'Доля пользователей первой недели {1-len(logs_exp_new.user_id.unique())/len(logs_exp.user_id.unique())}')
print()
print(logs_exp_new.groupby('group')['user_id'].nunique())
количество событий первой недели 2826 Доля событий первой недели 0.01159560630741896 Всего пользователей первой недели 17. Доля пользователей первой недели 0.0022513574361011646 group 246 2484 247 2513 248 2537 Name: user_id, dtype: int64
print(f'В среднем на пользователя приходится {int(len(logs_exp_new) / len(logs_exp_new.user_id.unique()))} событие.')
В среднем на пользователя приходится 31 событие.
# посчитаем перцентели по событиям на пользователя
round(logs_exp_new.groupby('user_id')[['event_name']].count().describe(percentiles=[0.05, 0.25, 0.5, 0.75, 0.95, 0.99]))
| event_name | |
|---|---|
| count | 7534.0 |
| mean | 32.0 |
| std | 65.0 |
| min | 1.0 |
| 5% | 3.0 |
| 25% | 9.0 |
| 50% | 19.0 |
| 75% | 37.0 |
| 95% | 88.0 |
| 99% | 201.0 |
| max | 2307.0 |
# проиллюстрируем данные графиком
plt.figure(figsize=(15, 7))
sns.histplot(data=logs_exp_new.groupby('user_id')[['event_name']].count(), x='event_name', color = "red", kde=True)
plt.title('Количество событий на одного пользователя')
plt.xlabel('Количество событий')
plt.ylabel('Количество пользователей')
plt.xlim(0,201)
plt.show()
Проанализируем взаимодействие пользователей с приложением.
# посмотрим количество событий пользователей по дням
session = logs_exp_new.groupby(['user_id', 'event_date'])['event_name'].count().reset_index()
session
| user_id | event_date | event_name | |
|---|---|---|---|
| 0 | 6888746892508752 | 2019-08-06 | 1 |
| 1 | 6909561520679493 | 2019-08-06 | 5 |
| 2 | 6922444491712477 | 2019-08-04 | 10 |
| 3 | 6922444491712477 | 2019-08-05 | 23 |
| 4 | 6922444491712477 | 2019-08-06 | 14 |
| ... | ... | ... | ... |
| 25729 | 9222603179720523844 | 2019-08-02 | 3 |
| 25730 | 9222603179720523844 | 2019-08-04 | 11 |
| 25731 | 9222603179720523844 | 2019-08-05 | 11 |
| 25732 | 9222603179720523844 | 2019-08-06 | 16 |
| 25733 | 9222603179720523844 | 2019-08-07 | 8 |
25734 rows × 3 columns
# посчитаем количество сессий на пользователя в день, используя метод describe
session.groupby('user_id')['event_date'].count().describe(percentiles=[0.05, 0.25, 0.5, 0.75, 0.95, 0.99]).round(2)
count 7534.00 mean 3.42 std 1.85 min 1.00 5% 1.00 25% 2.00 50% 3.00 75% 5.00 95% 7.00 99% 7.00 max 7.00 Name: event_date, dtype: float64
#используя метод describe посчитаем количество событий за сессию
session.event_name.describe(percentiles=[0.05, 0.25, 0.5, 0.75, 0.95, 0.99]).round(2)
count 25734.00 mean 9.36 std 23.56 min 1.00 5% 1.00 25% 3.00 50% 6.00 75% 11.00 95% 24.00 99% 52.00 max 2190.00 Name: event_name, dtype: float64
Решение отбросить данные за последнюю неделю июля 2019 года было верным. Потери в данных для анализа не более 1,2%.
При анализе взаимодействия пользователей с приложением было выяснено следующее:
Посмотрим, какие события есть в логах, как часто они встречаются. Отсортируем события по частоте. Посчитаем, сколько пользователей совершали каждое из этих событий. Отсортируем события по числу пользователей. Посчитаем долю пользователей, которые хоть раз совершали событие.
# Для каждого события подсчитаем, какое количество пользователей в каждой группе его совершила
user_events = logs_exp_new.pivot_table(index='event_name', columns='group',values='user_id', aggfunc='nunique', margins=True)\
.reset_index().sort_values('All', ascending=False).reset_index(drop=True)
user_events.set_axis(['event_name', '246','247','248','All'], axis='columns', inplace=True)
user_events['A_A'] = user_events['246'] + user_events['247']
user_events
| event_name | 246 | 247 | 248 | All | A_A | |
|---|---|---|---|---|---|---|
| 0 | All | 2484 | 2513 | 2537 | 7534 | 4997 |
| 1 | MainScreenAppear | 2450 | 2476 | 2493 | 7419 | 4926 |
| 2 | OffersScreenAppear | 1542 | 1520 | 1531 | 4593 | 3062 |
| 3 | CartScreenAppear | 1266 | 1238 | 1230 | 3734 | 2504 |
| 4 | PaymentScreenSuccessful | 1200 | 1158 | 1181 | 3539 | 2358 |
| 5 | Tutorial | 278 | 283 | 279 | 840 | 561 |
Мы определили, что всего в воронке уникальных событий пять:
Просмотр главной страницы является самым популярным событием. Пользователи, зашедшие на главную страницу, совершили хотя бы одно событие. Таких пользователей 7419 (98,47%).
Будем считать, что при движении по воронке пользователи проходят 4 события. Обучающую страницу (пятое событие) пользователи посещают редко, её посещение или игнорирование никак не сказывается на переходе на следующий шаг. Видимо, нет условия посетить её обязательно. Это разумно, ведь новым пользователь может считаться только в первое посещение ресурса. Даже учитывая это обстоятельство, количество посетителей этой страницы почти в 10 раз меньше количества уникальных посетителей сайта. Поэтому в визуализацию воронки продаж эту страницу включать не будем.
#данные для воронки
funnel = user_events.drop([5], axis=0)
funnel
| event_name | 246 | 247 | 248 | All | A_A | |
|---|---|---|---|---|---|---|
| 0 | All | 2484 | 2513 | 2537 | 7534 | 4997 |
| 1 | MainScreenAppear | 2450 | 2476 | 2493 | 7419 | 4926 |
| 2 | OffersScreenAppear | 1542 | 1520 | 1531 | 4593 | 3062 |
| 3 | CartScreenAppear | 1266 | 1238 | 1230 | 3734 | 2504 |
| 4 | PaymentScreenSuccessful | 1200 | 1158 | 1181 | 3539 | 2358 |
# визуализируем воронку продаж
fig = go.Figure(go.Funnel(
y = (funnel['event_name']),
x = (funnel['All']),
textposition = "inside",
textinfo = "value+percent initial",
opacity = 0.65, marker = {"color": ["deepskyblue", "lightsalmon", "tan", "teal", "silver"],
"line": {"width": [4, 2, 2, 3, 1, 1], "color": ["wheat", "wheat", "blue", "wheat", "wheat"]}},
connector = {"line": {"color": "royalblue", "dash": "dot", "width": 3}})
)
fig.add_trace(go.Funnel(
name = 'CR',
orientation = "h",
y = (funnel['event_name']),
x = (funnel['All']),
textposition = "inside",
textinfo = "value+percent previous"))
fig.show()
# визуализируем воронку продаж группы А/А теста
fig = go.Figure(go.Funnel(
y = (funnel['event_name']),
x = (funnel['A_A']),
textposition = "inside",
textinfo = "value+percent initial",
opacity = 0.65, marker = {"color": ["deepskyblue", "lightsalmon", "tan", "teal", "silver"],
"line": {"width": [4, 2, 2, 3, 1, 1], "color": ["wheat", "wheat", "blue", "wheat", "wheat"]}},
connector = {"line": {"color": "royalblue", "dash": "dot", "width": 3}})
)
fig.add_trace(go.Funnel(
name = 'CR',
orientation = "h",
y = (funnel['event_name']),
x = (funnel['A_A']),
textposition = "inside",
textinfo = "value+percent previous"))
fig.show()
# визуализируем воронку продаж экспериментальной группы
fig = go.Figure(go.Funnel(
y = (funnel['event_name']),
x = (funnel['248']),
textposition = "inside",
textinfo = "value+percent initial",
opacity = 0.65, marker = {"color": ["deepskyblue", "lightsalmon", "tan", "teal", "silver"],
"line": {"width": [4, 2, 2, 3, 1, 1], "color": ["wheat", "wheat", "blue", "wheat", "wheat"]}},
connector = {"line": {"color": "royalblue", "dash": "dot", "width": 3}})
)
fig.add_trace(go.Funnel(
name = 'CR',
orientation = "h",
y = (funnel['event_name']),
x = (funnel['248']),
textposition = "inside",
textinfo = "value+percent previous"))
fig.show()
Последовательной цепочкой событий для пользователей можно считать 4 шага: просмотр главной страницы, просмотр страницы предложений, просмотр корзины и переход к успешной оплате. Функции страницы просмотр обучающей информации для новых пользователей не понятны,процент заходящих туда пользователей очень мал. Эта страница не встраивается в последовательную цепочку маршрута пользователя.
Анализ воронки продаж.
Мы построили 3 воронки продаж: общую по всем пользователям эксперимента, по группам А/А теста и по экспериментальной группе. Расхождения по всем этапам воронки по каждой визуализации - не более 2%.
Рекомендации для маркетологов и технической поддержки
Разработчики решили поменять шрифты во всём приложении. Но перед тем нужно обязательно изучить возможную реакцию пользователей на такие изменения. Поэтому необходимо провести А/А/В тест. Группы 246 и 247 будут являться тестовыми, а группа 248 - экспериментальной. В данном разделе мы будем использовать данные за первую неделю августа 2019 года. Выведем на экран количество пользователей в каждой из экспирементальных групп.
print(logs_exp_new.groupby('group')['user_id'].nunique())
group 246 2484 247 2513 248 2537 Name: user_id, dtype: int64
Критерии успешного A/A-теста:
Проверим наши группы по этим критериям.
print('Численность групп 246 и 247 отличается на ', round(100-2484/2513*100,2),'%')
print('Численность групп 247 и 248 отличается на ', round(100-2513/2537*100,2),'%')
print('Численность групп 248 и 246 отличается на ', round(100-2537/2484*100,2),'%')
Численность групп 246 и 247 отличается на 1.15 % Численность групп 247 и 248 отличается на 0.95 % Численность групп 248 и 246 отличается на -2.13 %
Итак, разница в численности групп 0,95%,1,15% и 2,13%. Группы довольно многочисленные. При отсутствии статистической значимости ключевых метрик по группам результаты эксперимента будут корректны.
Пункт 2 мы проверить не можем - примем его на веру.
Пользователи, попавших в обе группы отсутствуют.
Осталось проверить критерий наличие или отсутствие статистической значимости ключевых метрик по группам.
Проанализируем А/А тест и А/В тесты последовательно.
Статистический анализ А/А теста
Сформулируем гипотезы:
Но: между долями пользователей, совершивших определённое событие, нет статистически значимой разницы между группами A/A теста;
Н1: между долями пользователей, совершивших определённое событие, есть статистически значимая разница между группами A/A теста.
Проверим гипотезы на первом событии.
# критический уровень статистической значимости
alpha = 0.05
# данные по количеству уникальных участников взяты из таблицы user_events по участникам, совершившим первое событие
# и общему количеству участников групп 246 и 247
successes = np.array([2450, 2476])
trials = np.array([2484, 2513])
# пропорция успехов в первой группе:
p1 = successes[0]/trials[0]# ваш код
# пропорция успехов во второй группе:
p2 = successes[1]/trials[1]
# пропорция успехов в комбинированном датасете:
p_combined = (successes[0] + successes[1]) / (trials[0] + trials[1])
# разница пропорций в датасетах
difference = p1 - p2
z_value = difference / mth.sqrt(p_combined * (1 - p_combined) * (1/trials[0] + 1/trials[1]))
# задаем стандартное нормальное распределение (среднее 0, ст.отклонение 1)
distr = st.norm(0, 1)
p_value = (1 - distr.cdf(abs(z_value))) * 2
print('p-значение: ', p_value)
if p_value < alpha:
print('Отвергаем нулевую гипотезу: между группами есть значимая разница')
else:
print(
'Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными'
)
p-значение: 0.7570597232046099 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными
Вывод: Между долями пользователей, дошедших до "посадочной"страницы сайта, нет статистической разницы между группами А/А теста. Создадим функцию для проверки различий между группами по всем событиям
Посчитаем статистическую разницу по всем событиям между группами 246-247.
Для этого напишем функцию.
Посчитаем статистическую разницу по всем событиям между группами 247-248 и 246-248.
Сформулируем гипотезы:
Но: между долями пользователей, совершивших определённое событие, нет статистически значимой разницы между всеми группами теста;
Н1: между долями пользователей, совершивших определённое событие, есть статистически значимая разница между всеми группами теста.
Проверим гипотезы с помощью функции.
#создадим функцию для проверки различий между группами по каждому событию
def check_statistics(successes1, successes2, trials1, trials2):
# пропорция успехов в первой группе:
alpha = 0.05
p1 = successes1/trials1
# пропорция успехов во второй группе:
p2 = successes2/trials2
# пропорция успехов в комбинированном датасете:
p_combined = (successes1 + successes2) / (trials1 + trials2)
# разница пропорций в датасетах
difference = p1 - p2
# считаем статистику в ст.отклонениях стандартного нормального распределения
z_value = difference / (p_combined * (1 - p_combined) * (1/trials1 + 1/trials2))**0.5
# задаем стандартное нормальное распределение (среднее 0, ст.отклонение 1)
distr = st.norm(0, 1)
p_value = (1 - distr.cdf(abs(z_value))) * 2
print('p-значение: ', p_value)
print('Результат теста: ')
if p_value < alpha:
print('Отвергаем нулевую гипотезу: между группами есть значимая разница')
else:
print(
'Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными'
)
print(' ')
return p_value
p_value
0.7570597232046099
# сравним 4 пары групп
for j in range(1, 5):
print('Пара', j)
print('===========')
if j == 3:
k = 1
else:
k = j + 1
if j == 4:
j = 3
k = 5
for i in range(1, 5):
result = check_statistics(funnel.iloc[i, j],
funnel.iloc[i, k],
funnel.iloc[0, j],
funnel.iloc[0, k])
Пара 1 =========== p-значение: 0.7570597232046099 Результат теста: Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными p-значение: 0.2480954578522181 Результат теста: Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными p-значение: 0.22883372237997213 Результат теста: Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными p-значение: 0.11456679313141849 Результат теста: Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными Пара 2 =========== p-значение: 0.4587053616621515 Результат теста: Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными p-значение: 0.9197817830592261 Результат теста: Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными p-значение: 0.5786197879539783 Результат теста: Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными p-значение: 0.7373415053803964 Результат теста: Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными Пара 3 =========== p-значение: 0.2949721933554552 Результат теста: Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными p-значение: 0.20836205402738917 Результат теста: Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными p-значение: 0.07842923237520116 Результат теста: Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными p-значение: 0.2122553275697796 Результат теста: Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными Пара 4 =========== p-значение: 0.29424526837179577 Результат теста: Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными p-значение: 0.43425549655188256 Результат теста: Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными p-значение: 0.18175875284404386 Результат теста: Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными p-значение: 0.6004294282308704 Результат теста: Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными
Таким образом, ни в одном из тестов не удалось отвергнуть нулевую гипотезу.
Шрифт не влияет на взаимодействие пользователя с приложением.
Учтём также,что в нашем эксперименте 4 пары групп и 4 этапа в воронке. Значит, число гипотез равно 16. Проверим корректность выводов методом Бонферрони.
def p_adjust_bh(p):
"""Benjamini-Hochberg p-value correction for multiple hypothesis testing."""
p = np.asfarray(p)
by_descend = p.argsort()[::-1]
by_orig = by_descend.argsort()
steps = float(len(p)) / np.arange(len(p), 0, -1)
q = np.minimum(1, np.minimum.accumulate(steps * p[by_descend]))
return q[by_orig]
result = p_adjust_bh([0.76, 0.25, 0.23, 0.11, 0.46, 0.92, 0.58, 0.74, 0.29, 0.21, 0.08, 0.21, 0.29, 0.43, 0.18, 0.60])
result
array([0.81066667, 0.51555556, 0.51555556, 0.51555556, 0.66909091,
0.92 , 0.73846154, 0.81066667, 0.51555556, 0.51555556,
0.51555556, 0.51555556, 0.51555556, 0.66909091, 0.51555556,
0.73846154])
Все выведенные значения значительно больше уровня статистической значимости 0,05, принятого в эксперименте.
Данные расчёты подтверждают, что мы не можем отвергнуть нулевую гипотезу даже учитывая групповую возможность ошибки первого рода.
Шрифт не влияет на взаимодействие пользователя с приложением.
Рекомендации для маркетологов и технической поддержки.